/**
 * MojoSetup; a portable, flexible installation application.
 *
 * Please see the file LICENSE.txt in the source's root directory.
 *
 *  This file written by Ryan C. Gordon.
     Copyright (c) 2006-2010 Ryan C. Gordon and others.

   This software is provided 'as-is', without any express or implied warranty.
   In no event will the authors be held liable for any damages arising from
   the use of this software.

   Permission is granted to anyone to use this software for any purpose,
   including commercial applications, and to alter it and redistribute it
   freely, subject to the following restrictions:

   1. The origin of this software must not be misrepresented; you must not
   claim that you wrote the original software. If you use this software in a
   product, an acknowledgment in the product documentation would be
   appreciated but is not required.

   2. Altered source versions must be plainly marked as such, and must not be
   misrepresented as being the original software.

   3. This notice may not be removed or altered from any source distribution.

       Ryan C. Gordon <icculus@icculus.org>

 */

#if !SUPPORT_GUI_COCOA
#error Something is wrong in the build system.
#endif

#import <Cocoa/Cocoa.h>
#undef true
#undef false

#define BUILDING_EXTERNAL_PLUGIN 1
#include "gui.h"

MOJOGUI_PLUGIN(cocoa)

#if !GUI_STATIC_LINK_COCOA
CREATE_MOJOGUI_ENTRY_POINT(cocoa)
#endif

typedef enum
{
    CLICK_BACK=-1,
    CLICK_CANCEL,
    CLICK_NEXT,
    CLICK_NONE
} ClickValue;

// This nasty hack is because we appear to need to be under
//  -[NSApp run] when calling things like NSRunAlertPanel().
// So we push a custom event, call -[NSApp run], catch it, do
//  the panel, then call -[NSApp stop]. Yuck.
typedef enum
{
    CUSTOMEVENT_BASEVALUE=3234,
    CUSTOMEVENT_RUNQUEUE,
    CUSTOMEVENT_MSGBOX,
    CUSTOMEVENT_PROMPTYN,
    CUSTOMEVENT_PROMPTYNAN,
    CUSTOMEVENT_INSERTMEDIA,
} CustomEvent;


static NSAutoreleasePool *GAutoreleasePool = nil;

@interface MojoSetupController : NSView
{
    IBOutlet NSButton *BackButton;
    IBOutlet NSButton *CancelButton;
    IBOutlet NSComboBox *DestinationCombo;
    IBOutlet NSTextField *FinalText;
    IBOutlet NSWindow *MainWindow;
    IBOutlet NSButton *NextButton;
    IBOutlet NSProgressIndicator *ProgressBar;
    IBOutlet NSTextField *ProgressComponentLabel;
    IBOutlet NSTextField *ProgressItemLabel;
    IBOutlet NSTextView *ReadmeText;
    IBOutlet NSTabView *TabView;
    IBOutlet NSTextField *TitleLabel;
    IBOutlet NSMenuItem *QuitMenuItem;
    IBOutlet NSMenuItem *AboutMenuItem;
    IBOutlet NSMenuItem *HideMenuItem;
    IBOutlet NSMenuItem *WindowMenuItem;
    IBOutlet NSMenuItem *HideOthersMenuItem;
    IBOutlet NSMenuItem *ShowAllMenuItem;
    IBOutlet NSMenuItem *ServicesMenuItem;
    IBOutlet NSMenuItem *MinimizeMenuItem;
    IBOutlet NSMenuItem *ZoomMenuItem;
    IBOutlet NSMenuItem *BringAllToFrontMenuItem;
    IBOutlet NSView *OptionsView;
    ClickValue clickValue;
    boolean canForward;
    boolean needToBreakEventLoop;
    boolean finalPage;
    MojoGuiYNAN answerYNAN;
    MojoGuiSetupOptions *mojoOpts;
}
- (void)awakeFromNib;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
- (void)prepareWidgets:(const char*)winTitle;
- (void)unprepareWidgets;
- (void)fireCustomEvent:(CustomEvent)eventType data1:(NSInteger)data1 data2:(NSInteger)data2 atStart:(BOOL)atStart;
- (void)doCustomEvent:(NSEvent *)event;
- (void)doMsgBox:(const char *)title text:(const char *)text;
- (void)doPromptYN:(const char *)title text:(const char *)text;
- (void)doPromptYNAN:(const char *)title text:(const char *)text;
- (void)doInsertMedia:(const char *)medianame;
- (MojoGuiYNAN)getAnswerYNAN;
- (IBAction)backClicked:(NSButton *)sender;
- (IBAction)cancelClicked:(NSButton *)sender;
- (IBAction)nextClicked:(NSButton *)sender;
- (IBAction)browseClicked:(NSButton *)sender;
- (IBAction)menuQuit:(NSMenuItem *)sender;
- (int)doPage:(NSString *)pageId title:(const char *)_title canBack:(boolean)canBack canFwd:(boolean)canFwd canCancel:(boolean)canCancel canFwdAtStart:(boolean)canFwdAtStart shouldBlock:(BOOL)shouldBlock;
- (int)doReadme:(const char *)title text:(NSString *)text canBack:(boolean)canBack canFwd:(boolean)canFwd;
- (void)setOptionTreeSensitivity:(MojoGuiSetupOptions *)opts enabled:(boolean)val;
- (void)optionToggled:(id)toggle;
- (NSView *)newOptionLevel:(NSView *)box;
- (void)buildOptions:(MojoGuiSetupOptions *)opts view:(NSView *)box sensitive:(boolean)sensitive;
- (int)doOptions:(MojoGuiSetupOptions *)opts canBack:(boolean)canBack canFwd:(boolean)canFwd;
- (char *)doDestination:(const char **)recommends recnum:(int)recnum command:(int *)command canBack:(boolean)canBack canFwd:(boolean)canFwd;
- (int)doProductKey:(const char *)desc fmt:(const char *)fmt buf:(char *)buf buflen:(const int)buflen canBack:(boolean)canBack canFwd:(boolean)canFwd;
- (int)doProgress:(const char *)type component:(const char *)component percent:(int)percent item:(const char *)item canCancel:(boolean)canCancel;
- (void)doFinal:(const char *)msg;
@end // interface MojoSetupController

@implementation MojoSetupController
    - (void)awakeFromNib
    {
        clickValue = CLICK_NONE;
        canForward = false;
        answerYNAN = MOJOGUI_NO;
        needToBreakEventLoop = false;
        finalPage = false;
        mojoOpts = nil;
    } // awakeFromNib

    - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
    {
        printf("didfinishlaunching\n");
        [NSApp stop:self];  // break out of NSApp::run()
    } // applicationDidFinishLaunching

    - (void)prepareWidgets:(const char*)winTitle
    {
        #if 1
        [BackButton setTitle:[NSString stringWithUTF8String:_("Back")]];
        [NextButton setTitle:[NSString stringWithUTF8String:_("Next")]];
        [CancelButton setTitle:[NSString stringWithUTF8String:_("Cancel")]];
        #else
        // !!! FIXME: there's probably a better way to do this.
        // Set the correct localization for the buttons, then resize them so
        //  the new text fits perfectly. After that, we need to reposition
        //  them so they don't look scattered.
        NSRect frameBack = [BackButton frame];
        NSRect frameNext = [NextButton frame];
        NSRect frameCancel = [CancelButton frame];
        const float startX = frameCancel.origin.x + frameCancel.size.width;
        const float spacing = (frameBack.origin.x + frameBack.size.width) - frameNext.origin.x;
        [BackButton setTitle:[NSString stringWithUTF8String:_("Back")]];
        [NextButton setTitle:[NSString stringWithUTF8String:_("Next")]];
        [CancelButton setTitle:[NSString stringWithUTF8String:_("Cancel")]];
        [BackButton sizeToFit];
        [NextButton sizeToFit];
        [CancelButton sizeToFit];
        frameBack = [BackButton frame];
        frameNext = [NextButton frame];
        frameCancel = [CancelButton frame];
        frameCancel.origin.x = startX - frameCancel.size.width;
        frameNext.origin.x = (frameCancel.origin.x - frameNext.size.width) - spacing;
        frameBack.origin.x = (frameNext.origin.x - frameBack.size.width) - spacing;
        [CancelButton setFrame:frameCancel];
        [CancelButton setNeedsDisplay:YES];
        [NextButton setFrame:frameNext];
        [NextButton setNeedsDisplay:YES];
        [BackButton setFrame:frameBack];
        [BackButton setNeedsDisplay:YES];
        #endif

        [ProgressBar setUsesThreadedAnimation:YES];  // we don't pump fast enough.
        [ProgressBar startAnimation:self];

        [WindowMenuItem setTitle:[NSString stringWithUTF8String:_("Window")]];
        [HideOthersMenuItem setTitle:[NSString stringWithUTF8String:_("Hide Others")]];
        [ShowAllMenuItem setTitle:[NSString stringWithUTF8String:_("Show All")]];
        [ServicesMenuItem setTitle:[NSString stringWithUTF8String:_("Services")]];
        [MinimizeMenuItem setTitle:[NSString stringWithUTF8String:_("Minimize")]];
        [ZoomMenuItem setTitle:[NSString stringWithUTF8String:_("Zoom")]];
        [BringAllToFrontMenuItem setTitle:[NSString stringWithUTF8String:_("Bring All to Front")]];

        NSString *appName;
        appName = (NSString *) [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
        if (appName == nil)
            appName = [[NSProcessInfo processInfo] processName];

        const char *utf8AppName = [appName UTF8String];
        char *text;

        text = format(_("About %0"), utf8AppName);
        [AboutMenuItem setTitle:[NSString stringWithUTF8String:text]];
        free(text);

        text = format(_("Hide %0"), utf8AppName);
        [HideMenuItem setTitle:[NSString stringWithUTF8String:text]];
        free(text);

        text = format(_("Quit %0"), utf8AppName);
        [QuitMenuItem setTitle:[NSString stringWithUTF8String:text]];
        free(text);

        [MainWindow setTitle:[NSString stringWithUTF8String:winTitle]];
        [MainWindow center];
        [MainWindow makeKeyAndOrderFront:self];
    } // prepareWidgets

    - (void)unprepareWidgets
    {
        [MainWindow orderOut:self];
    } // unprepareWidgets

    - (void)fireCustomEvent:(CustomEvent)eventType data1:(NSInteger)data1 data2:(NSInteger)data2 atStart:(BOOL)atStart
    {
        NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:nil subtype:(short)eventType data1:data1 data2:data2];
        [NSApp postEvent:event atStart:atStart];
        [NSApp run];  // event handler _must_ call -[NSApp stop], or you block here forever.
    } // fireCustomEvent

    - (void)doCustomEvent:(NSEvent*)event
    {
        printf("custom event!\n");
        switch ((CustomEvent) [event subtype])
        {
            case CUSTOMEVENT_RUNQUEUE:
                if ([NSApp modalWindow] != nil)
                {
                    // If we're in a modal thing, so don't break the event loop.
                    //  Just make a note to break it later.
                    needToBreakEventLoop = true;
                    return;
                } // if
                break;  // we just need the -[NSApp stop] call.
            case CUSTOMEVENT_MSGBOX:
                [self doMsgBox:(const char *)[event data1] text:(const char *)[event data2]];
                break;
            case CUSTOMEVENT_PROMPTYN:
                [self doPromptYN:(const char *)[event data1] text:(const char *)[event data2]];
                break;
            case CUSTOMEVENT_PROMPTYNAN:
                [self doPromptYNAN:(const char *)[event data1] text:(const char *)[event data2]];
                break;
            case CUSTOMEVENT_INSERTMEDIA:
                [self doInsertMedia:(const char *)[event data1]];
                break;
            default:
                return;  // let it go without breaking the event loop.
        } // switch

        [NSApp stop:self];  // break the event loop.
    } // doCustomEvent

    - (void)doMsgBox:(const char *)title text:(const char *)text
    {
        NSString *titlestr = [NSString stringWithUTF8String:title];
        NSString *textstr = [NSString stringWithUTF8String:text];
        NSString *okstr = [NSString stringWithUTF8String:_("OK")];
        NSRunInformationalAlertPanel(titlestr, textstr, okstr, nil, nil);
        if (needToBreakEventLoop)
        {
            needToBreakEventLoop = false;
            [self fireCustomEvent:CUSTOMEVENT_RUNQUEUE data1:0 data2:0 atStart:NO];
        } // if
    } // doMsgBox

    - (void)doPromptYN:(const char *)title text:(const char *)text
    {
        NSString *titlestr = [NSString stringWithUTF8String:title];
        NSString *textstr = [NSString stringWithUTF8String:text];
        NSString *yesstr = [NSString stringWithUTF8String:_("Yes")];
        NSString *nostr = [NSString stringWithUTF8String:_("No")];
        const NSInteger rc = NSRunAlertPanel(titlestr, textstr, yesstr, nostr, nil);
        answerYNAN = ((rc == NSAlertDefaultReturn) ? MOJOGUI_YES : MOJOGUI_NO);
        if (needToBreakEventLoop)
        {
            needToBreakEventLoop = false;
            [self fireCustomEvent:CUSTOMEVENT_RUNQUEUE data1:0 data2:0 atStart:NO];
        } // if
    } // doPromptYN

    - (void)doPromptYNAN:(const char *)title text:(const char *)text
    {
        // !!! FIXME
        [self doPromptYN:title text:text];
    } // doPromptYN

    - (void)doInsertMedia:(const char *)medianame
    {
        NSString *title = [NSString stringWithUTF8String:_("Media change")];
        char *fmt = xstrdup(_("Please insert '%0'"));
        char *_text = format(fmt, medianame);
        NSString *text = [NSString stringWithUTF8String:_text];
        free(_text);
        free(fmt);
        NSString *okstr = [NSString stringWithUTF8String:_("OK")];
        NSString *cancelstr = [NSString stringWithUTF8String:_("Cancel")];
        const NSInteger rc = NSRunAlertPanel(title, text, okstr, cancelstr, nil);
        answerYNAN = ((rc == NSAlertDefaultReturn) ? MOJOGUI_YES : MOJOGUI_NO);
        if (needToBreakEventLoop)
        {
            needToBreakEventLoop = false;
            [self fireCustomEvent:CUSTOMEVENT_RUNQUEUE data1:0 data2:0 atStart:NO];
        } // if
    } // doInsertMedia

    - (MojoGuiYNAN)getAnswerYNAN
    {
        return answerYNAN;
    } // getAnswerYNAN

    - (IBAction)backClicked:(NSButton *)sender
    {
        clickValue = CLICK_BACK;
        [NSApp stop:self];
    } // backClicked

    - (IBAction)cancelClicked:(NSButton *)sender
    {
        char *title = xstrdup(_("Cancel installation"));
        char *text = xstrdup(_("Are you sure you want to cancel installation?"));
        [self doPromptYN:title text:text];
        free(title);
        free(text);
        if (answerYNAN == MOJOGUI_YES)
        {
            clickValue = CLICK_CANCEL;
            [NSApp stop:self];
        } // if
    } // cancelClicked

    - (IBAction)nextClicked:(NSButton *)sender
    {
        clickValue = CLICK_NEXT;
        [NSApp stop:self];
    } // nextClicked

    - (IBAction)browseClicked:(NSButton *)sender
    {
        NSOpenPanel *panel = [NSOpenPanel openPanel];
        [panel setTitle:[NSString stringWithUTF8String:_("Destination")]];
        [panel setAllowsMultipleSelection:NO];
        [panel setCanCreateDirectories:YES];
        [panel setCanChooseDirectories:YES];
        [panel setCanChooseFiles:NO];
        if ([panel runModal] == NSOKButton)
            [DestinationCombo setStringValue:[panel filename]];
    } // browseClicked

    - (IBAction)menuQuit:(NSMenuItem *)sender
    {
        if (finalPage)  // make this work like you clicked "finished".
            [self nextClicked:nil];
        else if ([CancelButton isEnabled])  // make this work like you clicked "cancel".
            [self cancelClicked:nil];
    } // menuQuit

    - (int)doPage:(NSString *)pageId title:(const char *)_title canBack:(boolean)canBack canFwd:(boolean)canFwd canCancel:(boolean)canCancel canFwdAtStart:(boolean)canFwdAtStart shouldBlock:(BOOL)shouldBlock
    {
        [TitleLabel setStringValue:[NSString stringWithUTF8String:_title]];
        clickValue = CLICK_NONE;
        canForward = canFwd;
        [BackButton setEnabled:canBack ? YES : NO];
        [NextButton setEnabled:canFwdAtStart ? YES : NO];
        [CancelButton setEnabled:canCancel ? YES : NO];
        [TabView selectTabViewItemWithIdentifier:pageId];
        if (shouldBlock == NO)
            [self fireCustomEvent:CUSTOMEVENT_RUNQUEUE data1:0 data2:0 atStart:NO];
        else
        {
            [NSApp run];
            assert(clickValue < CLICK_NONE);
        } // else
        return (int) clickValue;
    } // doPage

    - (int)doReadme:(const char *)title text:(NSString *)text canBack:(boolean)canBack canFwd:(boolean)canFwd
    {
        NSRange range = {0, 1};  // reset scrolling to start of text.
        [ReadmeText setString:text];
        [ReadmeText scrollRangeToVisible:range];
        return [self doPage:@"Readme" title:title canBack:canBack canFwd:canFwd canCancel:true canFwdAtStart:canFwd shouldBlock:YES];
    } // doReadme

    - (void)setOptionTreeSensitivity:(MojoGuiSetupOptions *)opts enabled:(boolean)val
    {
        if (opts != nil)
        {
            [((id) opts->guiopaque) setEnabled:(val ? YES : NO)];
            [self setOptionTreeSensitivity:opts->next_sibling enabled:val];
            [self setOptionTreeSensitivity:opts->child enabled:(val && opts->value)];
        } // if
    } // setOptionTreeSensitivity

    - (MojoGuiSetupOptions *)findMojoOption:(id)obj opt:(MojoGuiSetupOptions *)opt
    {
        // !!! FIXME: this is not ideal. How can we attach this pointer to
        // !!! FIXME:  the objects themselves so we don't have to walk a tree
        // !!! FIXME:  to find it on each action? The objects are controls
        // !!! FIXME:  and cells (distinct classes), and I don't control the
        // !!! FIXME:  creation of all of them (radio buttons).
        // !!! FIXME: Alternately, let's just hold a hashtable to map
        // !!! FIXME:  objects to options without walking this tree.
        if (opt == nil)
            return nil;

        MojoGuiSetupOptions *i;
        for (i = opt; i != nil; i = i->next_sibling)
        {
            if (i->guiopaque == ((void *) obj))
                return i;
            MojoGuiSetupOptions *rc = [self findMojoOption:obj opt:i->child];
            if (rc != nil)
                return rc;
        } // for

        return [self findMojoOption:obj opt:opt->child];
    } // findMojoOption

    - (void)optionToggled:(id)toggle
    {
        MojoGuiSetupOptions *opts = [self findMojoOption:toggle opt:mojoOpts];
        assert(opts != nil);
        // !!! FIXME: cast is wrong. use a selector?
        const boolean enabled = ([((NSControl*)toggle) isEnabled] == YES);
        opts->value = enabled;
        [self setOptionTreeSensitivity:opts->child enabled:enabled];
    } // optionToggled

    - (NSView *)newOptionLevel:(NSView *)box
    {
        NSRect frame = NSMakeRect(10, 10, 10, 10);
        NSView *widget = [[NSView alloc] initWithFrame:frame];
        [box addSubview:widget positioned:NSWindowBelow relativeTo:nil];
        [widget release];  // (box) owns it now.
        return widget;
    } // newOptionLevel

    // !!! FIXME: most of this mess is cut, pasted, and Cocoaized from the
    // !!! FIXME:  GTK+ GUI. Can we abstract this in the high level and just
    // !!! FIXME:  implement the target-specific bits in the plugins?
    - (void)buildOptions:(MojoGuiSetupOptions *)opts view:(NSView *)box sensitive:(boolean)sensitive
    {
        NSRect frame = NSMakeRect(10, 10, 10, 10);
        if (opts != nil)
        {
            if (opts->is_group_parent)
            {
                MojoGuiSetupOptions *kids = opts->child;
                NSView *childbox = nil;
                //GtkWidget *alignment = gtk_alignment_new(0.0, 0.5, 0, 0);
                //gtk_widget_show(alignment);

                // !!! FIXME: disable line wrap?
                // !!! FIXME: resize on superview resize?
                NSTextField *widget = [[NSTextField alloc] initWithFrame:frame];
                [widget setStringValue:[NSString stringWithUTF8String:opts->description]];
                [widget setEnabled:(sensitive ? YES : NO)];
                [widget setEditable:NO];
                [widget setSelectable:NO];
                [widget setBordered:NO];
                [widget setBezeled:NO];
                [widget setAlignment:NSLeftTextAlignment];
                [widget sizeToFit];
                if (opts->tooltip != nil)
                    [widget setToolTip:[NSString stringWithUTF8String:opts->tooltip]];
                [box addSubview:widget positioned:NSWindowBelow relativeTo:nil];
                [widget release];  // (box) owns it now.
                //!!! FIXME[box sizeToFit];

                childbox = [self newOptionLevel:box];
                NSButtonCell *prototype = [[NSButtonCell alloc] init];
                [prototype setButtonType:NSRadioButton];
                [prototype setAllowsMixedState:NO];
                NSMatrix *matrix = [[NSMatrix alloc] initWithFrame:frame mode:NSRadioModeMatrix prototype:(NSCell *)prototype numberOfRows:0 numberOfColumns:1];
                int row = 0;
                while (kids)
                {
                    [matrix addRow];
                    NSButtonCell *cell = (NSButtonCell *) [matrix cellAtRow:row column:0];
                    kids->guiopaque = cell;
                    [cell setTitle:[NSString stringWithUTF8String:kids->description]];
                    [matrix setState:(kids->value ? NSOnState : NSOffState) atRow:row column:0];
                    [cell setEnabled:(kids->value ? YES : NO)];
                    [cell setTarget:self];
                    [cell setAction:@selector(optionToggled:)];

                    if (kids->tooltip != nil)
                        [matrix setToolTip:[NSString stringWithUTF8String:kids->tooltip] forCell:cell];

                    if (kids->child != nil)
                        [self buildOptions:kids->child view:[self newOptionLevel:childbox] sensitive:sensitive];

                    kids = kids->next_sibling;
                    row++;
                } // while

                [matrix sizeToCells];
                [childbox addSubview:matrix positioned:NSWindowBelow relativeTo:nil];
                [matrix release];  // childbox owns it now.
                //!!! FIXME: [childbox sizeToFit];
                //!!! FIXME: [[childbox superview] sizeToFit];
            } // if

            else
            {
                NSButton *widget = [[NSButton alloc] initWithFrame:frame];
                opts->guiopaque = widget;
                [widget setAllowsMixedState:NO];
                [widget setTitle:[NSString stringWithUTF8String:opts->description]];
                [widget setState:(opts->value ? NSOnState : NSOffState)];
                [widget setEnabled:(sensitive ? YES : NO)];
                [widget setTarget:self];
                [widget setAction:@selector(optionToggled:)];
                [box addSubview:widget positioned:NSWindowBelow relativeTo:nil];
                [widget release];  // (box) owns it now.
                //!!!FIXME:[box sizeToFit];

                if (opts->tooltip != nil)
                    [widget setToolTip:[NSString stringWithUTF8String:opts->tooltip]];

                if (opts->child != nil)
                    [self buildOptions:opts->child view:[self newOptionLevel:box] sensitive:((sensitive) && (opts->value))];
            } // else

            [self buildOptions:opts->next_sibling view:box sensitive:sensitive];
        } // if

        //!!! FIXME:[box sizeToFit];
    } // buildOptions

    - (int)doOptions:(MojoGuiSetupOptions *)opts canBack:(boolean)canBack canFwd:(boolean)canFwd
    {
        // add all the option widgets to the page's view.
        [self buildOptions:opts view:OptionsView sensitive:true];

        // run the page.
        mojoOpts = opts;
        int retval = [self doPage:@"Options" title:_("Options") canBack:canBack canFwd:canFwd canCancel:true canFwdAtStart:canFwd shouldBlock:YES];
        mojoOpts = nil;

        // we're done, so nuke everything from the view.
        NSArray *array = [[OptionsView subviews] copy];
        NSEnumerator *enumerator = [array objectEnumerator];
        NSView *obj;
        while ((obj = (NSView *) [enumerator nextObject]) != nil)
            [obj removeFromSuperviewWithoutNeedingDisplay];
        [OptionsView setNeedsDisplay:YES];
        [enumerator release];
        [array release];

        return retval;
    } // doOptions

    - (char *)doDestination:(const char **)recommends recnum:(int)recnum command:(int *)command canBack:(boolean)canBack canFwd:(boolean)canFwd
    {
        const boolean fwdAtStart = ( (recnum > 0) && (*(recommends[0])) );
        int i;

        [DestinationCombo removeAllItems];
        for (i = 0; i < recnum; i++)
            [DestinationCombo addItemWithObjectValue:[NSString stringWithUTF8String:recommends[i]]];

        if (recnum > 0)
            [DestinationCombo setStringValue:[NSString stringWithUTF8String:recommends[0]]];
        else
            [DestinationCombo setStringValue:@""];

        *command = [self doPage:@"Destination" title:_("Destination") canBack:canBack canFwd:canFwd canCancel:true canFwdAtStart:fwdAtStart shouldBlock:YES];
        char *retval = xstrdup([[DestinationCombo stringValue] UTF8String]);
        [DestinationCombo removeAllItems];
        [DestinationCombo setStringValue:@""];
        return retval;
    } // doDestination

    - (int)doProductKey:(const char *)desc fmt:(const char *)fmt buf:(char *)buf buflen:(const int)buflen canBack:(boolean)canBack canFwd:(boolean)canFwd
    {
        // !!! FIXME: write me!
        return [self doPage:@"ProductKey" title:desc canBack:canBack canFwd:canFwd canCancel:true canFwdAtStart:canFwd shouldBlock:YES];
    } // doProductKey

    - (int)doProgress:(const char *)type component:(const char *)component percent:(int)percent item:(const char *)item canCancel:(boolean)canCancel
    {
        const BOOL indeterminate = (percent < 0) ? YES : NO;
        [ProgressComponentLabel setStringValue:[NSString stringWithUTF8String:component]];
        [ProgressItemLabel setStringValue:[NSString stringWithUTF8String:item]];
        [ProgressBar setIndeterminate:indeterminate];
        if (!indeterminate)
            [ProgressBar setDoubleValue:(double)percent];
        return [self doPage:@"Progress" title:type canBack:false canFwd:false canCancel:canCancel canFwdAtStart:false shouldBlock:NO];
    } // doProgress

    - (void)doFinal:(const char *)msg
    {
        finalPage = true;
        [FinalText setStringValue:[NSString stringWithUTF8String:msg]];
        [NextButton setTitle:[NSString stringWithUTF8String:_("Finish")]];
        [self doPage:@"Final" title:_("Finish") canBack:false canFwd:true canCancel:false canFwdAtStart:true shouldBlock:YES];
    } // doFinal
@end // implementation MojoSetupController

// Override [NSApplication sendEvent], so we can catch custom events.
@interface MojoSetupApplication : NSApplication
{
}
- (void)sendEvent:(NSEvent *)event;
@end // interface MojoSetupApplication

@implementation MojoSetupApplication
    - (void)sendEvent:(NSEvent *)event
    {
        if ([event type] == NSApplicationDefined)
            [((MojoSetupController *)[self delegate]) doCustomEvent:event];
        [super sendEvent:event];
    } // sendEvent
@end // implementation MojoSetupApplication


static uint8 MojoGui_cocoa_priority(boolean istty)
{
    // obviously this is the thing you want on Mac OS X.
    return MOJOGUI_PRIORITY_TRY_FIRST;
} // MojoGui_cocoa_priority


static boolean MojoGui_cocoa_init(void)
{
    // This lets a stdio app become a GUI app. Otherwise, you won't get
    //  GUI events from the system and other things will fail to work.
    // Putting the app in an application bundle does the same thing.
    //  TransformProcessType() is a 10.3+ API. SetFrontProcess() is 10.0+.
    if (TransformProcessType != NULL)  // check it as a weak symbol.
    {
        ProcessSerialNumber psn = { 0, kCurrentProcess };
        TransformProcessType(&psn, kProcessTransformToForegroundApplication);
        SetFrontProcess(&psn);
    } // if

    GAutoreleasePool = [[NSAutoreleasePool alloc] init];

    // !!! FIXME: make sure we have access to the desktop...may be ssh'd in
    // !!! FIXME:  as another user that doesn't have the Finder loaded or
    // !!! FIXME:  something.

    // For NSApp to be our subclass, instead of default NSApplication.
    [MojoSetupApplication sharedApplication];
    if ([NSBundle loadNibNamed:@"MojoSetup" owner:NSApp] == NO)
        return false;

    // Force NSApp initialization stuff. MojoSetupController is set, in the
    //  .nib, to be NSApp's delegate. Its applicationDidFinishLaunching calls
    //  [NSApp stop] to break event loop right away so we can continue.
    [NSApp run];

    return true;  // always succeeds.
} // MojoGui_cocoa_init


static void MojoGui_cocoa_deinit(void)
{
    [GAutoreleasePool release];
    GAutoreleasePool = nil;
    // !!! FIXME: destroy nib and NSApp?
} // MojoGui_cocoa_deinit


static void MojoGui_cocoa_msgbox(const char *title, const char *text)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[NSApp delegate] fireCustomEvent:CUSTOMEVENT_MSGBOX data1:(NSInteger)title data2:(NSInteger)text atStart:YES];
    [pool release];
} // MojoGui_cocoa_msgbox


static boolean MojoGui_cocoa_promptyn(const char *title, const char *text,
                                      boolean defval)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[NSApp delegate] fireCustomEvent:CUSTOMEVENT_PROMPTYN data1:(NSInteger)title data2:(NSInteger)text atStart:YES];
    const MojoGuiYNAN ynan = [[NSApp delegate] getAnswerYNAN];
    [pool release];
    assert((ynan == MOJOGUI_YES) || (ynan == MOJOGUI_NO));
    return (ynan == MOJOGUI_YES);
} // MojoGui_cocoa_promptyn


static MojoGuiYNAN MojoGui_cocoa_promptynan(const char *title,
                                            const char *text,
                                            boolean defval)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[NSApp delegate] fireCustomEvent:CUSTOMEVENT_PROMPTYNAN data1:(NSInteger)title data2:(NSInteger)text atStart:YES];
    const MojoGuiYNAN retval = [[NSApp delegate] getAnswerYNAN];
    [pool release];
    return retval;
} // MojoGui_cocoa_promptynan


static boolean MojoGui_cocoa_start(const char *title,
                                   const MojoGuiSplash *splash)
{
printf("start\n");
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // !!! FIXME: deal with (splash).
    [[NSApp delegate] prepareWidgets:title];
    [pool release];
    return true;
} // MojoGui_cocoa_start


static void MojoGui_cocoa_stop(void)
{
printf("stop\n");
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[NSApp delegate] unprepareWidgets];
    [pool release];
} // MojoGui_cocoa_stop


static int MojoGui_cocoa_readme(const char *name, const uint8 *data,
                                    size_t len, boolean can_back,
                                    boolean can_fwd)
{
printf("readme\n");
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSString *str = [[[NSString alloc] initWithBytes:data length:len encoding:NSUTF8StringEncoding] autorelease];
    const int retval = [[NSApp delegate] doReadme:name text:str canBack:can_back canFwd:can_fwd];
    [pool release];
    return retval;
} // MojoGui_cocoa_readme


static int MojoGui_cocoa_options(MojoGuiSetupOptions *opts,
                       boolean can_back, boolean can_fwd)
{
printf("options\n");
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    const int retval = [[NSApp delegate] doOptions:opts canBack:can_back canFwd:can_fwd];
    [pool release];
    return retval;
} // MojoGui_cocoa_options


static char *MojoGui_cocoa_destination(const char **recommends, int recnum,
                                       int *command, boolean can_back,
                                       boolean can_fwd)
{
printf("destination\n");
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    char *retval = [[NSApp delegate] doDestination:recommends recnum:recnum command:command canBack:can_back canFwd:can_fwd];
    [pool release];
    return retval;
} // MojoGui_cocoa_destination


static int MojoGui_cocoa_productkey(const char *desc, const char *fmt,
                                    char *buf, const int buflen,
                                    boolean can_back, boolean can_fwd)
{
printf("productkey\n");
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    const int retval = [[NSApp delegate] doProductKey:desc fmt:fmt buf:buf buflen:buflen canBack:can_back canFwd:can_fwd];
    [pool release];
    return retval;
} // MojoGui_cocoa_productkey


static boolean MojoGui_cocoa_insertmedia(const char *medianame)
{
printf("insertmedia\n");
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[NSApp delegate] fireCustomEvent:CUSTOMEVENT_INSERTMEDIA data1:(NSInteger)medianame data2:0 atStart:YES];
    const MojoGuiYNAN ynan = [[NSApp delegate] getAnswerYNAN];
    assert((ynan == MOJOGUI_YES) || (ynan == MOJOGUI_NO));
    [pool release];
    return (ynan == MOJOGUI_YES);
} // MojoGui_cocoa_insertmedia


static void MojoGui_cocoa_progressitem(void)
{
printf("progressitem\n");
    // no-op in this UI target.
} // MojoGui_cocoa_progressitem


static int MojoGui_cocoa_progress(const char *type, const char *component,
                                  int percent, const char *item,
                                  boolean can_cancel)
{
printf("progress\n");
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    const int retval = [[NSApp delegate] doProgress:type component:component percent:percent item:item canCancel:can_cancel];
    [pool release];
    return retval;
} // MojoGui_cocoa_progress


static void MojoGui_cocoa_final(const char *msg)
{
printf("final\n");
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[NSApp delegate] doFinal:msg];
    [pool release];
} // MojoGui_cocoa_final

// end of gui_cocoa.m ...

